/**
 * @copyright https://github.com/nodejs/node/blob/master/lib/events.js
 */
/* eslint-disable */
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

var objectCreate = Object.create || objectCreatePolyfill;
var objectKeys = Object.keys || objectKeysPolyfill;
var bind = Function.prototype.bind || functionBindPolyfill;

// By default Dispatchers will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
let defaultMaxListeners = 10;

export default class EventEmitter {
    static listenerCount(emitter, type) {
        if (typeof emitter.listenerCount === 'function') {
            return emitter.listenerCount(type);
        } else {
            return listenerCount.call(emitter, type);
        }
    }

    static defaultMaxListeners: number;

    _maxListeners?: number;
    _eventsCount: number;
    _events: any;

    constructor() {
        if (!this._events || !Object.prototype.hasOwnProperty.call(this, '_events')) {
            this._events = objectCreate(null);
            this._eventsCount = 0;
        }

        this._maxListeners = this._maxListeners || undefined;
    }

    // // Obviously not all Emitters should be limited to 10. This function allows
    // // that to be increased. Set to zero for unlimited.
    // setMaxListeners(n) {
    //     if (typeof n !== 'number' || n < 0 || isNaN(n))
    //         throw new TypeError('"n" argument must be a positive number');
    //     this._maxListeners = n;
    //     return this;
    // }
    //
    // getMaxListeners() {
    //     return $getMaxListeners(this);
    // }

    emit(type: string, ...rest) {
        var er, handler, len, args, i, events;
        var doError = type === 'error';

        events = this._events;
        if (events) doError = doError && events.error == null;
        else if (!doError) return false;

        // If there is no 'error' event listener then throw.
        if (doError) {
            if (arguments.length > 1) er = arguments[1];
            if (er instanceof Error) {
                throw er; // Unhandled 'error' event
            } else {
                // At least give some kind of context to the user
                var err = new Error('Unhandled "error" event. (' + er + ')');
                err['context'] = er;
                throw err;
            }
        }

        handler = events[type];

        if (!handler) return false;

        var isFn = typeof handler === 'function';
        len = arguments.length;
        switch (len) {
            // fast cases
            case 1:
                emitNone(handler, isFn, this);
                break;
            case 2:
                emitOne(handler, isFn, this, arguments[1]);
                break;
            case 3:
                emitTwo(handler, isFn, this, arguments[1], arguments[2]);
                break;
            case 4:
                emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);
                break;
            // slower
            default:
                args = new Array(len - 1);
                for (i = 1; i < len; i++) args[i - 1] = arguments[i];
                emitMany(handler, isFn, this, args);
        }

        return true;
    }

    on(type: string, listener: Function) {
        return _addListener(this, type, listener);
    }

    once(type, listener) {
        if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function');
        this.on(type, _onceWrap(this, type, listener));
        return this;
    }

    off(type, listener) {
        return _removeListener.call(this, type, listener);
    }

    removeAllListeners(type) {
        var listeners, events, i;

        events = this._events;
        if (!events) return this;

        // not listening for off, no need to emit
        if (!events.off) {
            if (arguments.length === 0) {
                this._events = objectCreate(null);
                this._eventsCount = 0;
            } else if (events[type]) {
                if (--this._eventsCount === 0) this._events = objectCreate(null);
                else delete events[type];
            }
            return this;
        }

        // emit off for all listeners on all events
        if (arguments.length === 0) {
            var keys = objectKeys(events);
            var key;
            for (i = 0; i < keys.length; ++i) {
                key = keys[i];
                if (key === 'off') continue;
                this.removeAllListeners(key);
            }
            this.removeAllListeners('off');
            this._events = objectCreate(null);
            this._eventsCount = 0;
            return this;
        }

        listeners = events[type];

        if (typeof listeners === 'function') {
            this.off(type, listeners);
        } else if (listeners) {
            // LIFO order
            for (i = listeners.length - 1; i >= 0; i--) {
                this.off(type, listeners[i]);
            }
        }

        return this;
    }

    listeners(type) {
        return _listeners(this, type, true);
    }

    rawListeners(type) {
        return _listeners(this, type, false);
    }

    listenerCount() {
        return EventEmitter.listenerCount.apply(this, arguments);
    }
}

let hasDefineProperty;
try {
    var o = {};
    if (Object.defineProperty) Object.defineProperty(o, 'x', { value: 0 });
    hasDefineProperty = o['x'] === 0;
} catch (err) {
    hasDefineProperty = false;
}
if (hasDefineProperty) {
    Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
        enumerable: true,
        get: function () {
            return defaultMaxListeners;
        },
        set: function (arg) {
            // check whether the input is a positive number (whose value is zero or
            // greater and not a NaN).
            if (typeof arg !== 'number' || arg < 0 || arg !== arg)
                throw new TypeError('"defaultMaxListeners" must be a positive number');
            defaultMaxListeners = arg;
        }
    });
} else {
    EventEmitter.defaultMaxListeners = defaultMaxListeners;
}

function $getMaxListeners(that) {
    if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners;
    return that._maxListeners;
}

// These standalone emit* functions are used to optimize calling of event
// handlers for fast cases because emit() itself often has a variable number of
// arguments and can be deoptimized because of that. These functions always have
// the same number of arguments and thus do not get deoptimized, so the code
// inside them can execute faster.
function emitNone(handler, isFn, self) {
    if (isFn) handler.call(self);
    else {
        var len = handler.length;
        var listeners = arrayClone(handler, len);
        for (var i = 0; i < len; ++i) {
            try {
                listeners[i].call(self);
            } catch (e) {
                console.error(e);
            }
        }
    }
}

function emitOne(handler, isFn, self, arg1) {
    if (isFn) handler.call(self, arg1);
    else {
        var len = handler.length;
        var listeners = arrayClone(handler, len);
        for (var i = 0; i < len; ++i) {
            try {
                listeners[i].call(self, arg1);
            } catch (e) {
                console.error(e);
            }
        }
    }
}

function emitTwo(handler, isFn, self, arg1, arg2) {
    if (isFn) handler.call(self, arg1, arg2);
    else {
        var len = handler.length;
        var listeners = arrayClone(handler, len);
        for (var i = 0; i < len; ++i) {
            try {
                listeners[i].call(self, arg1, arg2);
            } catch (e) {
                console.error(e);
            }
        }
    }
}

function emitThree(handler, isFn, self, arg1, arg2, arg3) {
    if (isFn) handler.call(self, arg1, arg2, arg3);
    else {
        var len = handler.length;
        var listeners = arrayClone(handler, len);
        for (var i = 0; i < len; ++i) {
            try {
                listeners[i].call(self, arg1, arg2, arg3);
            } catch (e) {
                console.error(e);
            }
        }
    }
}

function emitMany(handler, isFn, self, args) {
    if (isFn) handler.apply(self, args);
    else {
        var len = handler.length;
        var listeners = arrayClone(handler, len);
        for (var i = 0; i < len; ++i) {
            try {
                listeners[i].apply(self, args);
            } catch (e) {
                console.error(e);
            }
        }
    }
}

function _addListener(target: EventEmitter, type, listener: Function) {
    var m;
    var events;
    var existing;

    if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function');

    events = target._events;
    if (!events) {
        events = target._events = objectCreate(null);
        target._eventsCount = 0;
    } else {
        // To avoid recursion in the case that type === "newListener"! Before
        // adding it to the listeners, first emit "newListener".
        if (events.newListener) {
            target.emit('newListener', type, listener['listener'] ? listener['listener'] : listener);

            // Re-assign `events` because a newListener handler could have caused the
            // this._events to be assigned to a new object
            events = target._events;
        }
        existing = events[type];
    }

    if (!existing) {
        // Optimize the case of one listener. Don't need the extra array object.
        existing = events[type] = listener;
        ++target._eventsCount;
    } else {
        if (typeof existing === 'function') {
            // Adding the second element, need to change to array.
            existing = events[type] = [existing, listener];
        } else {
            existing.push(listener);
        }

        // Check for listener leak
        if (!existing.warned) {
            m = $getMaxListeners(target);
            if (m && m > 0 && existing.length > m) {
                existing.warned = true;

                class CustomError extends Error {
                    emitter: any;
                    type: string;
                    count: number;
                }
                let w: CustomError = new CustomError(
                    'Possible Dispatcher memory leak detected. ' +
                        existing.length +
                        ' "' +
                        String(type) +
                        '" listeners ' +
                        'added. Use emitter.setMaxListeners() to ' +
                        'increase limit.'
                );
                w.name = 'MaxListenersExceededWarning';
                w.emitter = target;
                w.type = type;
                w.count = existing.length;

                if (typeof console === 'object' && console.warn) {
                    console.warn('%s: %s', w.name, w.message);
                }
            }
        }
    }

    return target;
}

function _removeListener(type, listener) {
    var list, events, position, i, originalListener;

    if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function');

    events = this._events;
    if (!events) return this;

    list = events[type];
    if (!list) return this;

    if (list === listener || list.listener === listener) {
        if (--this._eventsCount === 0) this._events = objectCreate(null);
        else {
            delete events[type];
            if (events.off) this.emit('off', type, list.listener || listener);
        }
    } else if (typeof list !== 'function') {
        position = -1;

        for (i = list.length - 1; i >= 0; i--) {
            if (list[i] === listener || list[i].listener === listener) {
                originalListener = list[i].listener;
                position = i;
                break;
            }
        }

        if (position < 0) return this;

        if (position === 0) list.shift();
        else spliceOne(list, position);

        if (list.length === 1) events[type] = list[0];

        if (events.off) this.emit('off', type, originalListener || listener);
    }

    return this;
}

function onceWrapper() {
    if (!this.fired) {
        this.target.off(this.type, this.wrapFn);
        this.fired = true;
        switch (arguments.length) {
            case 0:
                return this.listener.call(this.target);
            case 1:
                return this.listener.call(this.target, arguments[0]);
            case 2:
                return this.listener.call(this.target, arguments[0], arguments[1]);
            case 3:
                return this.listener.call(this.target, arguments[0], arguments[1], arguments[2]);
            default:
                var args = new Array(arguments.length);
                for (var i = 0; i < args.length; ++i) args[i] = arguments[i];
                this.listener.apply(this.target, args);
        }
    }
}

function _onceWrap(target, type, listener) {
    var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
    var wrapped = bind.call(onceWrapper, state);
    wrapped.listener = listener;
    state.wrapFn = wrapped;
    return wrapped;
}

function _listeners(target, type, unwrap) {
    var events = target._events;

    if (!events) return [];

    var evlistener = events[type];
    if (!evlistener) return [];

    if (typeof evlistener === 'function') return unwrap ? [evlistener.listener || evlistener] : [evlistener];

    return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
}

function listenerCount(type) {
    var events = this._events;

    if (events) {
        var evlistener = events[type];

        if (typeof evlistener === 'function') {
            return 1;
        } else if (evlistener) {
            return evlistener.length;
        }
    }

    return 0;
}

// About 1.5x faster than the two-arg version of Array#splice().
function spliceOne(list, index) {
    for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) list[i] = list[k];
    list.pop();
}

function arrayClone(arr, n) {
    var copy = new Array(n);
    for (var i = 0; i < n; ++i) copy[i] = arr[i];
    return copy;
}

function unwrapListeners(arr) {
    var ret = new Array(arr.length);
    for (var i = 0; i < ret.length; ++i) {
        ret[i] = arr[i].listener || arr[i];
    }
    return ret;
}

function objectCreatePolyfill(proto) {
    var F = function () {};
    F.prototype = proto;
    return new F();
}

function objectKeysPolyfill(obj) {
    var keys = [];
    for (var k in obj)
        if (Object.prototype.hasOwnProperty.call(obj, k)) {
            keys.push(k);
        }
    return k;
}

function functionBindPolyfill(context) {
    var fn = this;
    return function () {
        return fn.apply(context, arguments);
    };
}
